package fr.n7.sma.view;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import fr.n7.sma.model.Agent;
import fr.n7.sma.model.AgentAction;
import fr.n7.sma.model.Attractor;
import fr.n7.sma.model.Cell;
import fr.n7.sma.model.GridModel;
import fr.n7.sma.model.AgentAction.Action;
import fr.n7.sma.model.listeners.GridModelListener;
import fr.n7.sma.model.listeners.events.AgentEvent;
import fr.n7.sma.model.listeners.events.CellEvent;
import fr.n7.sma.model.listeners.events.GridModelEvent;
import fr.n7.sma.util.FiniteSizeLinkedList;
import fr.n7.sma.util.FiniteSizeListener;
import fr.n7.sma.util.property.PropertyChangedEvent;
import fr.n7.sma.util.property.PropertyListener;
import fr.n7.sma.util.property.PropertyManager;
import fr.n7.sma.view.model.AgentViewProperties;
import fr.n7.sma.view.model.CellViewProperties;
import fr.n7.sma.view.model.TraceModel;

public class Grid
extends JPanel
implements 	GridModelListener<AgentViewProperties, CellViewProperties>,
ComponentListener,
FiniteSizeListener<TraceModel>,
PropertyListener<Grid.Properties>,
MouseWheelListener, MouseListener{

	private static final long serialVersionUID = -3806884221303621862L;

	public static int DEFAULT_MEMORY_LENGTH = 10;

	public static final double ZOOM_MIN_BEFORE_REMOVING_IMAGES = 3;

	public static final double ZOOM_MIN_BEFORE_REMOVING_LINES = 4;

	private GridModel<Cell<CellViewProperties>, AgentViewProperties, CellViewProperties> gridModel;

	private List<Trace>[][] traces;

	private BufferedImage backgroundImg;
	private BufferedImage foregroundImg;

	private ResourceCharts resourceCharts;

	private static final float MAX_FORCE = 500f;

	private static final float MIDDLE_FORCE = 50f;



	public static enum Properties
	{
		DRAW_AGENTS,
		PUT_PHERO,
		USE_ATTRACTOR,
		DRAW_ATTRACTOR,
		EVAPORATION_RATE_PERCENT,
		DRAW_RESOURCES,
		DRAW_MEMORY,
		PHERO_FORCE,
		PHERO_RADIUS,
		DRAW_LINES_PREFERENCE,
		DRAW_IMAGES_FOR_RESOURCES_PREFERENCE,
		DRAW_IMAGES_FOR_AGENTS,
//		RESOURCE_SYMBOLS,
//		XZOOM,
//		YZOOM,
		REAL_TIME,
		MEMORY_LENGTH,
		AGENT_NUMBER,
		RESOURCE_NUMBER,
		ADDABLE_RESOURCES,
		RECOGNISED_RESOURCES,
		RESOURCES,
		GRID_REFRESH_RATE,
		CHARTS_REFRESH_RATE,
		PAUSE,
		EVAP_ON,
		VISION
	}

	//Options
	private PropertyManager<Properties> props;

	private boolean drawLines = true;
	private boolean drawImagesForResources = true;

	private Map<String, BufferedImage> resourceSymbols;

	private double xzoom = 12;
	private double yzoom = 12;

	private int xoffset = 0;
	private int yoffset = 0;

	private Stroke tail;

	private Map<String, Color> resourcesColors;

	@SuppressWarnings("unchecked")
	public Grid(
			GridModel<Cell<CellViewProperties>, AgentViewProperties, CellViewProperties> g,
			Map<String, BufferedImage> resourceSymbols,
			Map<String, Color> resourceColors,
			Dimension d,
			PropertyManager<Properties> propertyManager,
			int memoryLength,
			ResourceCharts resourceCharts) {
		setDoubleBuffered(true);
		this.availableAgentIDs = new ArrayList();
		this.maxAvailableAgentID = 0;
		this.props = propertyManager;
		props.addListener(this);

		this.gridModel = g;
		setMinimumSize(new Dimension(gridModel.width, gridModel.height));
		if(props.get(Properties.REAL_TIME, Boolean.class)){
			gridModel.addListener(this);
		}
		this.addComponentListener(this);
		this.resourceSymbols = resourceSymbols;
		this.resourcesColors = resourceColors;
		setPreferredSize(d);
		setLayout(null);

		this.resourceCharts = resourceCharts;
		// --- Agents ---
		int nbAgents = propertyManager.get(Properties.AGENT_NUMBER, Integer.class);
		addAgent(nbAgents);

		traces = (List<Trace>[][])new List[gridModel.width][gridModel.height];
		for(int x=0; x<gridModel.width; x++){
			for(int y=0; y<gridModel.height; y++){
				traces[x][y] = new ArrayList<Trace>();
			}
		}

//		addMouseWheelListener(this);
	}

	private List<String> availableAgentIDs;
	private int maxAvailableAgentID;

	@SuppressWarnings("unchecked")
	public void addAgent(int number){

		synchronized (gridModel.getAgents()) {
			int w = gridModel.width;
			int h = gridModel.height;

			ArrayList<Agent<AgentViewProperties, CellViewProperties>> as = 
				new ArrayList<Agent<AgentViewProperties,CellViewProperties>>();
			int memoryLength = props.get(Properties.MEMORY_LENGTH, Integer.class);
			String name;
			for(int i=0; i<number; i++){
				if(availableAgentIDs.isEmpty()){
					name = "A-"+(maxAvailableAgentID++);
				}else{
					name = availableAgentIDs.get(0);
					availableAgentIDs.remove(0);
				}

				short x = (short)(w*Math.random());
				short y = (short)(h*Math.random());
				short[] p = {x, y};
				Agent<AgentViewProperties, CellViewProperties> a = 
					new Agent<AgentViewProperties, CellViewProperties>(
							name,
							gridModel,
							p,
							props.get(Properties.RECOGNISED_RESOURCES, List.class),
							props.get(Properties.MEMORY_LENGTH, Integer.class),
							props.get(Properties.USE_ATTRACTOR, Boolean.class),
							props.get(Properties.PUT_PHERO, Boolean.class));
				a.addResourcesListener(resourceCharts);
				Color agentColor = new Color((int)(Math.pow(2, 24)*Math.random()));
				BufferedImage im;
				try {
					im = ImageIO.read(new File("resources/32/"+agentPngs[(int)(Math.random()*agentPngs.length)]+".png"));
					if(props.get(Properties.DRAW_IMAGES_FOR_AGENTS, Boolean.class)){
						a.setAdditionnalProperty(new AgentViewProperties(agentColor, im));
					}else{
						a.setAdditionnalProperty(new AgentViewProperties(agentColor, im, (int)xzoom, (int)yzoom));
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
				JLabel l = a.getAdditionnalProperty().getLabel();
				l.setToolTipText(a.getName());
				l.addMouseMotionListener(new AgentLabelMouseListener(this, a));
				l.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
				add(l);

				gridModel.addAgent(a);

				FiniteSizeLinkedList<TraceModel> fsll = new FiniteSizeLinkedList<TraceModel>(2*memoryLength);
				fsll.addListener(this);
				a.getAdditionnalProperty().setTraces(fsll);
				as.add(a);
			}
			drawImagesForAgentsChanged(as);
			agentsOnChanged(as);
		}
	}

	public void removeAgents(int number){
		for(int i=0; i<number; i++){
			Agent<AgentViewProperties, CellViewProperties> a = gridModel.removeRandomAgent();
			remove(a.getAdditionnalProperty().getLabel());
			availableAgentIDs.add(a.getName());
		}
		if(props.get(Properties.DRAW_MEMORY, Boolean.class)){
			synchronized (traces) {
				for(int j=0; j<traces.length; j++){
					for(int k=0; k<traces[0].length; k++){
						traces[j][k].clear();
					}
				}
			}
		}
	}

	public void setResourcesColors(Map<String, Color> resourcesColors) {
		this.resourcesColors = resourcesColors;
	}


	public void paintComponent(Graphics g) {
		applyImages(g);
	}

	public void completeRedrawAndResize(){
		initializeImgs();
		completeRedraw();
	}

	public void completeRedraw(){
		if (SwingUtilities.isEventDispatchThread()) {
			new ImageInitializer().run();
			new BackgroundDrawer().run();
			new ForegroundDrawer().run();
		}else{
			SwingUtilities.invokeLater(new ImageInitializer());
			SwingUtilities.invokeLater(new BackgroundDrawer());
			SwingUtilities.invokeLater(new ForegroundDrawer());
		}
		repaint();
	}

	public void waitCompleteRedrawAndResize(){
		waitInitializeImgs();
		waitCompleteRedraw();
	}

	public void waitCompleteRedraw(){
		if (SwingUtilities.isEventDispatchThread()) {
			new ImageInitializer().run();
			new BackgroundDrawer().run();
			new ForegroundDrawer().run();
		}else{
			try {
				SwingUtilities.invokeAndWait(new ImageInitializer());
				SwingUtilities.invokeAndWait(new BackgroundDrawer());
				SwingUtilities.invokeAndWait(new ForegroundDrawer());
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}
		repaint();
	}

	private void applyImages(Graphics g){
		int w = getWidth();
		int h = getHeight();
		g.drawImage(backgroundImg.getSubimage(0, 0, w, h), 0, 0, this);
		if(props.get(Properties.DRAW_AGENTS, Boolean.class)
				|| props.get(Properties.DRAW_MEMORY, Boolean.class)){
			g.drawImage(foregroundImg.getSubimage(0, 0, w, h), 0, 0, this);
		}
	}

	private void drawMemory(Graphics2D g2d, int w, int h, int[] xmem,
			int[] ymem, int i0, int i1, Agent<AgentViewProperties, CellViewProperties> agent,
			boolean save) {
		int x0 = xmem[i0];
		int y0 = ymem[i0];
		int x1 = xmem[i1];
		int y1 = ymem[i1];
		float dx, dy;

		if (w<3 || ((x0 != w-1) || (x1 != 0)) && ((x1 != w-1) || (x0 != 0))) {
			dx = 0.5f * (1 + x1 - x0);
		}else if ((x0 == 0) && (x1 == w-1)) {
			dx = 0;
		}else{
			dx = 1;
		}

		if (h<3 || ((y0 != h-1) || (y1 != 0)) && ((y1 != h-1) || (y0 != 0))) {
			dy = 0.5f * (1 + y1 - y0);
		}else if ((y0 == 0) && (y1 == h-1)) {
			dy = 0;
		}else{
			dy = 1;
		}

		if(save){
			drawTraceAndSaveIt(g2d, x0, y0, dx		, dy		, agent);
			drawTraceAndSaveIt(g2d, x1, y1, 1f - dx	, 1f - dy	, agent);
		}else{
			drawTraceWithRepaint(g2d, x0, y0, dx, dy);
			drawTraceWithRepaint(g2d, x1, y1, 1f - dx, 1f - dy);
		}

	}

	/**
	 * It doesn't change the color of the g2d, it must be initialized before.
	 * @param g2d
	 * @param x
	 * @param y
	 * @param dx
	 * @param dy
	 * @param agent
	 */
	public void drawTraceAndSaveIt(Graphics2D g2d, int x, int y, float dx, float dy, Agent<AgentViewProperties, CellViewProperties> agent){
		Color color = agent.getAdditionnalProperty().getColor();

		drawTraceWithRepaint(g2d, x, y, dx, dy);

		Trace t = new Trace(dx, dy, color);
		synchronized(traces){
			traces[x][y].add(t);
		}
		agent.getAdditionnalProperty().getTraces().add(new TraceModel(x, y, agent, t));
	}

	/**
	 * It doesn't change the color of the g2d, it must be initialized before.
	 * @param g2d
	 * @param x
	 * @param y
	 * @param dx
	 * @param dy
	 */
	private void drawTraceWithRepaint(Graphics2D g2d, int x, int y, float dx, float dy){

		int x00 = (int) (xzoom * (x + 0.5));
		int y00 = (int) (yzoom * (y + 0.5));
		int x01 = (int) (xzoom * (x + dx));
		int y01 = (int) (yzoom * (y + dy));
		g2d.drawLine(x00, y00, x01, y01);

		repaint(x00, y00, x01, y01);
		repaint(x01, y01, x00, y00);

	}

	private void drawTraceWithoutRepaint(Graphics2D g2d, int x, int y, float dx, float dy){

		int x00 = (int) (xzoom * (x + 0.5));
		int y00 = (int) (yzoom * (y + 0.5));
		int x01 = (int) (xzoom * (x + dx));
		int y01 = (int) (yzoom * (y + dy));
		g2d.drawLine(x00, y00, x01, y01);

	}

	private void drawCell(Cell<CellViewProperties> c, Graphics2D graphics){
		boolean pherosOn = props.get(Properties.DRAW_ATTRACTOR, Boolean.class);
		boolean resourcesOn = props.get(Properties.DRAW_RESOURCES, Boolean.class);
		if(pherosOn || resourcesOn){

			short[] p = c.getPosition();
			int x = (p[0] + xoffset) % gridModel.width;
			int y = (p[1] + yoffset) % gridModel.height;

			int x1 = (int) (xzoom * x);
			int y1 = (int) (yzoom * y);

			int x2 = (int)(xzoom*(x+1)) - (int)(xzoom*x);
			int y2 = (int)(yzoom*(y+1)) - (int)(yzoom*y);

			Rectangle r = graphics.getClipBounds();

			if(r==null || !((r.x > x2 && r.y > y2) || (r.x + r.width < x1 && r.y + r.height < y1))){
				Map<String, Short> res = c.getResources();

				if(drawLines){
					x1 += 1;
					y1 += 1;
					x2 -= 1;
					y2 -= 1;
				}

				graphics.setColor(Color.white);
				graphics.fillRect(x1, y1, x2, y2);

				if (pherosOn) {
					float force = Math.min(c.getAttractorEffects() + c.getPheroEffects(), MAX_FORCE);
					if(force < MIDDLE_FORCE){
						graphics.setColor(new Color(0f, 1f, 0f, force/MIDDLE_FORCE));
						graphics.fillRect(x1, y1, x2, y2);
					}else{
						float a = ((force-MIDDLE_FORCE) / (MAX_FORCE-MIDDLE_FORCE));
						graphics.setColor(new Color(0f, 1f - a, 0f, 1f));
						graphics.fillRect(x1, y1, x2, y2);
					}
				}
				if(resourcesOn){
					if(drawImagesForResources){
						int l = Math.min((int) (xzoom - 2), (int) (yzoom - 2));
						int lx = 1 + ((int) (xzoom - 1) - l) / 2;
						int ly = 1 + ((int) (yzoom - 1) - l) / 2;

						for (String s : res.keySet()) {
							if(res.get(s)>0){
								graphics.drawImage(resourceSymbols.get(s), x1 + lx, y1
										+ ly, l, l, null);
								break;
							}
						}
					}else{
						int lx = 0;
						int ly = 0;
						if (pherosOn) {
							lx = (int)(xzoom / 4.);
							ly = (int)(yzoom / 4.);
						}
						for (String s : res.keySet()) {
							if(res.get(s)>0){
								graphics.setColor(resourcesColors.get(s));
								graphics.fillRect(x1+lx, y1+ly, x2-2*lx, y2-2*ly);
							}
						}
					}
				}
				repaint(x1, y1, x2, y2);
			}
		}
	}

	public void locateAgent(short[] pos, Agent<AgentViewProperties, CellViewProperties> agent){

		int x = (pos[0] + xoffset) % gridModel.width;
		int y = (pos[1] + yoffset) % gridModel.height;

		if(props.get(Properties.DRAW_AGENTS, Boolean.class)){
			JLabel al = agent.getAdditionnalProperty().getLabel();
			Dimension d = al.getPreferredSize();
			al.setBounds((int) (xzoom * x + (xzoom - d.width) / 2),
					(int) (yzoom * y + (yzoom - d.height) / 2),
					d.width,
					d.height);
		}
	}

	public void gridModelChanged(GridModelEvent<AgentViewProperties, CellViewProperties> e) {
		switch(e.getEventType()){
		case CELL_CHANGED :
			CellEvent<CellViewProperties> ce = e.getCellEvent();
			Cell<CellViewProperties> c = (Cell<CellViewProperties>) e.getCellEvent().getCell();
			Graphics2D graphics = (Graphics2D) backgroundImg.getGraphics();

			short[] p = c.getPosition();
			int x = (p[0] + xoffset) % gridModel.width;
			int y = (p[1] + yoffset) % gridModel.height;

			drawCell(c, graphics);

			switch(ce.getEventType()){
			case ATTRACTOR_CHANGED :
				break;
			case RESOURCE_NUMBER_CHANGED :
				break;
			default : throw new Error("Cell Event not managed : "+ce.getEventType());
			}
			break;
		case AGENT_CHANGED :
			AgentEvent<AgentViewProperties, CellViewProperties> aevt = e.getAgentEvent();
			switch(aevt.getEventType()){
			case AGENT_MOVED :

				short[] pos = aevt.getTo();
				Agent<AgentViewProperties, CellViewProperties> agent = aevt.getAgent();

				x = (pos[0] + xoffset) % gridModel.width;
				y = (pos[1] + yoffset) % gridModel.height;

				locateAgent(pos, agent);

				if(props.get(Properties.DRAW_MEMORY, Boolean.class)){
					Graphics2D g2d = (Graphics2D)foregroundImg.getGraphics();
					g2d.setBackground(new Color(0,0,0,0));
					g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
							RenderingHints.VALUE_ANTIALIAS_ON);
					g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
							RenderingHints.VALUE_RENDER_QUALITY);
					//g2d.clearRect(0, 0, foregroundImg.getWidth(), foregroundImg.getHeight());
					int w = gridModel.width;
					int h = gridModel.height;
					short[] from = aevt.getFrom();

					int xf = (from[0] + xoffset) % gridModel.width;
					int yf = (from[1] + yoffset) % gridModel.height;

					g2d.setColor(agent.getAdditionnalProperty().getColor());
					g2d.setStroke(tail);
					int xmem[] = { xf, x };
					int ymem[] = { yf, y };
					drawMemory(g2d, w, h, xmem, ymem, 0, 1, agent, true);
				}
				break;
			case RESOURCE_TAKEN :
				if(props.get(Properties.DRAW_IMAGES_FOR_AGENTS, Boolean.class)){
					agent = aevt.getAgent();
					String rName = aevt.getResource();
					agent.getAdditionnalProperty().putResourceImageIcon(rName, resourceSymbols.get(rName));
				}
				break;
			case RESOURCE_PUT :
				if(props.get(Properties.DRAW_IMAGES_FOR_AGENTS, Boolean.class)){
					agent = aevt.getAgent();
					agent.getAdditionnalProperty().putOriginalImageIcon();
				}
				break;
			}
		}
	}

	public void setOffset(int x, int y) {
		int w = gridModel.width;
		while(x<0){
			x += w;
		}
		xoffset = x % w;
		int h = gridModel.height;
		while(y<0){
			y += h;
		}
		yoffset = y % h;
		waitCompleteRedraw();
	}

	public int getXOffset() {
		return xoffset;
	}

	public int getYOffset() {
		return yoffset;
	}

	public void setXOffset(int x) {
		int w = gridModel.width;
		while(x<0){
			x += w;
		}
		xoffset = x % w;
		waitCompleteRedraw();
	}

	public void setYOffset(int y) {
		int h = gridModel.height;
		while(y<0){
			y += h;
		}
		yoffset = y % h;
		waitCompleteRedraw();
	}

	public void componentHidden(ComponentEvent e) {
	}

	private class ForegroundDrawer implements Runnable {


		public void run() {
			Graphics2D foreGroundGraphics = (Graphics2D) foregroundImg.getGraphics();
			foreGroundGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
			foreGroundGraphics.setRenderingHint(RenderingHints.KEY_RENDERING,
					RenderingHints.VALUE_RENDER_QUALITY);

			boolean  drawMemory = props.get(Properties.DRAW_MEMORY, Boolean.class);
			synchronized (gridModel.getAgents()) {	
				Iterator<Agent<AgentViewProperties, CellViewProperties>> it = gridModel.getAgents().iterator();
				while (it.hasNext()) {
					Agent<AgentViewProperties, CellViewProperties> a = it.next();

					short[] apos = a.getPosition();
					locateAgent(apos, a);

					if(drawMemory){
						if(props.get(Properties.REAL_TIME, Boolean.class)){
							for(TraceModel tm : a.getAdditionnalProperty().getTraces()){
								Trace t = tm.getTrace();
								int[] pos = tm.getPos();
								foreGroundGraphics.setColor(t.color);
								foreGroundGraphics.setStroke(tail);
								drawTraceWithRepaint(foreGroundGraphics, pos[0], pos[1], t.dx, t.dy);
							}
						}else{
							Graphics2D g2d = (Graphics2D)foregroundImg.getGraphics();
							g2d.setBackground(new Color(0,0,0,0));
							g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
									RenderingHints.VALUE_ANTIALIAS_ON);
							g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
									RenderingHints.VALUE_RENDER_QUALITY);
							//g2d.clearRect(0, 0, foregroundImg.getWidth(), foregroundImg.getHeight());
							int w = gridModel.width;
							int h = gridModel.height;
							for(AgentAction aa : a.getLife()){
								if(aa.getAction() == Action.WALK){
									short[] from = aa.getPos();
									short[] to = aa.getTo();

									int xf = (from[0] + xoffset) % gridModel.width;
									int yf = (from[1] + yoffset) % gridModel.height;

									int x = (to[0] + xoffset) % gridModel.width;
									int y = (to[1] + yoffset) % gridModel.height;

									g2d.setColor(a.getAdditionnalProperty().getColor());
									g2d.setStroke(tail);
									int xmem[] = { xf, x };
									int ymem[] = { yf, y };
									drawMemory(g2d, w, h, xmem, ymem, 0, 1, a, false);
								}
							}
						}
					}
				}
			}
		}
	}

	private class BackgroundDrawer implements Runnable {


		public void run() {
			int panelH = getHeight();
			int panelW = getWidth();

			xzoom = (float)panelW/(float)gridModel.width;
			yzoom = (float)panelH/(float)gridModel.height;

			Graphics2D backgroundGraphics = (Graphics2D) backgroundImg.getGraphics();
			backgroundGraphics.setBackground(Color.WHITE);
			backgroundGraphics.setRenderingHint(
					RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
			backgroundGraphics.setRenderingHint(RenderingHints.KEY_RENDERING,
					RenderingHints.VALUE_RENDER_QUALITY);

			Cell<CellViewProperties>[][] cs = gridModel.getCells();
			int gw = gridModel.width;
			int gh = gridModel.height;

			backgroundGraphics.setColor(Color.LIGHT_GRAY);
			if(drawLines){
				// Draw the vertical lines
				int gh1 = (int) (yzoom * gh);
				for (int i = 0; i < gw; i++) {
					int x = (i + xoffset) % gw;
					int x1 = (int) (xzoom * x);
					backgroundGraphics.drawLine(x1, 0, x1, gh1);
				}
				// Draw the horizontal lines
				int gw1 = (int) (xzoom * gw);
				for (int j = 0; j < gh; j++) {
					int y = (j + yoffset) % gh;
					int y1 = (int) (yzoom * y);
					backgroundGraphics.drawLine(0, y1, gw1, y1);
				}
			}

			backgroundGraphics.setColor(Color.BLACK);
			int gh1 = (int) (yzoom * gh);
			int gw1 = (int) (xzoom * gw);

			int x0 = (int)(xzoom*xoffset);
			int y0 = (int)(yzoom*yoffset);

			backgroundGraphics.drawLine(x0, 0, x0, gh1);

			backgroundGraphics.drawLine(0, y0, gw1, y0);

			for (int i = 0; i < gw; i++) {
				for (int j = 0; j < gh; j++) {
					drawCell(cs[i][j], backgroundGraphics);
				}
			}
		}
	}

	private class ImageCreator implements Runnable {


		public void run() {
			int panelH = getHeight();
			int panelW = getWidth();
			if(panelH>0 && panelW>0){
//				xzoom = panelW * 1. / gridModel.width;
//				yzoom = panelH * 1. / gridModel.height;

				if(xzoom > 4 && yzoom > 4){
					float[] style = { (float) (0.2 * xzoom), (float) (0.2 * xzoom) };
					tail = new BasicStroke((float) (0.1f * xzoom), BasicStroke.CAP_BUTT,
							BasicStroke.JOIN_ROUND, 10.0f, style, 0);
				}else{
					float[] style = {1, 0};
					tail = new BasicStroke((float) Math.max(0.1, (0.2f * xzoom)), BasicStroke.CAP_BUTT,
							BasicStroke.JOIN_ROUND, 10.0f, style, 0);
				}

				drawImagesForResources = 
					((yzoom > ZOOM_MIN_BEFORE_REMOVING_IMAGES)
							&& (xzoom > ZOOM_MIN_BEFORE_REMOVING_IMAGES))
							&& props.get(Properties.DRAW_IMAGES_FOR_RESOURCES_PREFERENCE, Boolean.class);
				drawLines =
					((xzoom > ZOOM_MIN_BEFORE_REMOVING_LINES)
							&& (yzoom > ZOOM_MIN_BEFORE_REMOVING_LINES))
							&& props.get(Properties.DRAW_LINES_PREFERENCE, Boolean.class);
				if(backgroundImg == null){
					Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
					backgroundImg = new BufferedImage(d.width, d.height,
							BufferedImage.TYPE_4BYTE_ABGR);
				}
				if(foregroundImg == null){
					Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
					foregroundImg = new BufferedImage(d.width, d.height,
							BufferedImage.TYPE_4BYTE_ABGR);
				}
			}
		}
	}

	private class ImageInitializer implements Runnable {


		public void run() {
			int panelH = getHeight();
			int panelW = getWidth();

			Graphics2D foreGroundGraphics = (Graphics2D) foregroundImg.getGraphics();
			foreGroundGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
			foreGroundGraphics.setRenderingHint(RenderingHints.KEY_RENDERING,
					RenderingHints.VALUE_RENDER_QUALITY);

			foreGroundGraphics.setBackground(new Color(255, 255, 255, 0));
			foreGroundGraphics.clearRect(0, 0, panelW, panelH);


			Graphics2D backgroundGraphics = (Graphics2D) backgroundImg.getGraphics();
			backgroundGraphics.setBackground(Color.WHITE);
			backgroundGraphics.clearRect(0, 0, panelW, panelH);
			backgroundGraphics.setRenderingHint(
					RenderingHints.KEY_ANTIALIASING,
					RenderingHints.VALUE_ANTIALIAS_ON);
			backgroundGraphics.setRenderingHint(RenderingHints.KEY_RENDERING,
					RenderingHints.VALUE_RENDER_QUALITY);
		}

	}

	private void waitInitializeImgs() {
		if (SwingUtilities.isEventDispatchThread()) {
			new ImageCreator().run();
			new ImageInitializer().run();
		} else {
			try {
				SwingUtilities.invokeLater(new ImageCreator());
				SwingUtilities.invokeAndWait(new ImageInitializer());
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}
		drawImagesForAgentsChanged(gridModel.getAgents());
	}

	private void initializeImgs() {
		if (SwingUtilities.isEventDispatchThread()) {
			new ImageCreator().run();
			new ImageInitializer().run();
		} else {
			SwingUtilities.invokeLater(new ImageCreator());
			SwingUtilities.invokeLater(new ImageInitializer());
		}
		drawImagesForAgentsChanged(gridModel.getAgents());
	}

	public void componentMoved(ComponentEvent e) {}

	public void componentResized(ComponentEvent e) {
		completeRedrawAndResize();
	}

	public void componentShown(ComponentEvent e) {
	}


	public void finiteSizeListReduced(TraceModel t) {
		t.getFrom();
		int[] po = t.getPos();
		int x = po[0];
		int y = po[1];
		Trace t0 = t.getTrace();
		synchronized(traces){
			traces[x][y].remove(t0);
		}

		Graphics2D g2d = (Graphics2D)foregroundImg.getGraphics();

		g2d.setBackground(new Color(0,0,0,0));
		g2d.clearRect((int)(xzoom * x), (int)(yzoom * y),
				(int)(xzoom+1), (int)(yzoom+1));

		g2d.setStroke(tail);
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
				RenderingHints.VALUE_RENDER_QUALITY);

		g2d.setColor(Color.WHITE);
		g2d.setComposite(AlphaComposite.Clear);
		drawTraceWithoutRepaint(g2d, x, y, t0.dx, t0.dy);
		g2d.setComposite(AlphaComposite.Src);
		synchronized(traces){
			for(Trace tr : traces[x][y]){
				g2d.setColor(tr.color);
				drawTraceWithoutRepaint(g2d, x, y, tr.dx, tr.dy);
			}
		}
		repaint((int)(xzoom * x), (int)(yzoom * y),
				(int)(xzoom * (x+1)), (int)(yzoom * (y+1)));
	}

	//XXX
	String[] agentPngs = 
	{
			"face-angel",
			"face-crying",
			"face-devilish",
			"face-glasses",
			"face-grin",
			"face-kiss",
			"face-monkey",
			"face-plain",
			"face-sad",
			"face-smile",
			"face-smile-big",
			"face-surprise",
			"face-wink"
	};

	public void agentsOnChanged(Collection<Agent<AgentViewProperties, CellViewProperties>> as){
		synchronized (as) {
			for (Agent<AgentViewProperties, CellViewProperties> a : as) {
				a.getAdditionnalProperty().setLabelVisible(props.get(Properties.DRAW_AGENTS, Boolean.class));
			}
		}
	}

	public void drawImagesForAgentsChanged(Collection<Agent<AgentViewProperties, CellViewProperties>> as){
		synchronized(as){
			for (Agent<AgentViewProperties, CellViewProperties> a : as) {
				if(props.get(Properties.DRAW_IMAGES_FOR_AGENTS, Boolean.class)){
					String rName = a.getHeldResourceName();
					if(rName == null){
						a.getAdditionnalProperty().putOriginalImageIcon();
					}else{
						a.getAdditionnalProperty().putResourceImageIcon(rName, resourceSymbols.get(rName));
					}
				}else{
					a.getAdditionnalProperty().putSymbolIcon((int)xzoom, (int)yzoom);
				}
				locateAgent(a.getPosition(), a);
			}
		}
	}

	@SuppressWarnings("unchecked")

	public void propertyChanged(PropertyChangedEvent<Properties> e) {
		switch(e.getKey()){
		case DRAW_AGENTS:
		{
			agentsOnChanged(gridModel.getAgents());
			break;
		}
		case DRAW_IMAGES_FOR_AGENTS:
		{
			drawImagesForAgentsChanged(gridModel.getAgents());
//			completeRedraw();
			break;
		}
		case DRAW_MEMORY :
		case DRAW_ATTRACTOR :
		case DRAW_RESOURCES :
		{
			completeRedraw();
			break;
		}
		case DRAW_IMAGES_FOR_RESOURCES_PREFERENCE:
		{
			drawImagesForResources = 
				((yzoom > ZOOM_MIN_BEFORE_REMOVING_IMAGES)
						|| (xzoom > ZOOM_MIN_BEFORE_REMOVING_IMAGES))
						&& props.get(Properties.DRAW_IMAGES_FOR_RESOURCES_PREFERENCE, Boolean.class);
			waitCompleteRedraw();
			break;
		}
		case DRAW_LINES_PREFERENCE :
		{
			drawLines =
				((xzoom > ZOOM_MIN_BEFORE_REMOVING_LINES)
						|| (yzoom > ZOOM_MIN_BEFORE_REMOVING_LINES))
						&& props.get(Properties.DRAW_LINES_PREFERENCE, Boolean.class);
			waitCompleteRedraw();
			break;
		}
		case REAL_TIME :
		{
			if((Boolean)e.getValue()){
				gridModel.addListener(this);
			}else{
				gridModel.removeListener(this);
			}
			int w = gridModel.width;
			int h = gridModel.height;
			synchronized (traces) {
				for(int i=0; i<w; i++){
					for(int j=0; j<h; j++){
						traces[i][j].clear();
					}	
				}
			}
			for(Agent<AgentViewProperties, CellViewProperties> a : gridModel.getAgents()){
				a.getAdditionnalProperty().getTraces().clear();
			}
			waitCompleteRedraw();
			break;
		}
		case MEMORY_LENGTH :
		{
			for(Agent<AgentViewProperties, CellViewProperties> agent : gridModel.getAgents()){
				agent.getAdditionnalProperty().getTraces().setMaxLength(2*(Integer)e.getValue());
				agent.setMemoryLength((Integer)e.getValue());
			}
			break;
		}
		case AGENT_NUMBER :
		{
			int missingAgentNumber = props.get(Properties.AGENT_NUMBER, Integer.class) - gridModel.getAgents().size();
			if(missingAgentNumber < 0){
				removeAgents(-missingAgentNumber);
			}else{
				addAgent(missingAgentNumber);
			}
			completeRedraw();
			break;
		}
		case RESOURCE_NUMBER :
		{
			int w = gridModel.width;
			int h = gridModel.height;
			int desiredResourceNumber = props.get(Properties.RESOURCE_NUMBER, Integer.class);
			if(desiredResourceNumber > w*h){
				JOptionPane.showMessageDialog(null, "Impossible");
			}else{
				int missingResourceNumber;
				synchronized (gridModel) {
					Cell<CellViewProperties>[][] cs = gridModel.getCells();
					int resourceNb = 0;
					for(int i=0; i<w; i++){
						for(int j=0; j<h; j++){
							Map<String ,Short> rs = cs[i][j].getResources();
							for(String name : rs.keySet()){
								resourceNb += rs.get(name);
							}
						}
					}

					for(Agent<AgentViewProperties, CellViewProperties> a : gridModel.getAgents()){
						if(a.getHeldResourceName() != null) resourceNb++;
					}

					missingResourceNumber = desiredResourceNumber - resourceNb;
				}

				while(missingResourceNumber < 0){
					synchronized (gridModel) {
						short[] pos;
						Cell<CellViewProperties> c;
						do{
							pos = new short[]{(short)(Math.random()*w), (short)(Math.random()*h)};
							c = gridModel.getCellAt(pos);
						}while(c.isFree());
						Map<String, Short> rs = c.getResources();
						Iterator<String> nameIt = rs.keySet().iterator();
						String name = nameIt.next();
						while(rs.get(name) == 0) name = nameIt.next();
						c.removeOneResource(name);
					}

					missingResourceNumber += 1;
				}
				while(missingResourceNumber > 0){
					synchronized (gridModel) {
						short[] pos;
						do{
							pos = new short[]{(short)(Math.random()*w), (short)(Math.random()*h)};
						}while(!gridModel.getCellAt(pos).isFree());

						Cell<CellViewProperties> c = gridModel.getCellAt(pos);
						List<String> names = props.get(Properties.ADDABLE_RESOURCES, List.class);
						if(!names.isEmpty()){
							String name = names.get((int)(Math.random()*names.size()));
							c.addOneResource(name);
						}
					}
					missingResourceNumber -= 1;
				}
				if(!props.get(Properties.REAL_TIME, Boolean.class)){
					completeRedraw();
				}
			}
			break;
		}
		case ADDABLE_RESOURCES :
			break;
		case GRID_REFRESH_RATE :
			break;
		case CHARTS_REFRESH_RATE :
			break;
		case RECOGNISED_RESOURCES :
			break;
		case RESOURCES :
			break;
		case PAUSE :
			break;
		case USE_ATTRACTOR :
			for(Agent<AgentViewProperties, CellViewProperties> a : gridModel.getAgents()){
				a.useAttractor((Boolean)e.getValue());
			}
			break;
		case PUT_PHERO :
			for(Agent<AgentViewProperties, CellViewProperties> a : gridModel.getAgents()){
				a.putPhero((Boolean)e.getValue());
			}
			break;
		case EVAPORATION_RATE_PERCENT :
			gridModel.setEvaporationRate(1f - (Integer)e.getValue()/100f);
			break;
		case EVAP_ON :
			break;
		case VISION :
			gridModel.setVision((Integer)e.getValue());
			break;
		case PHERO_FORCE :
			gridModel.setPheroForce((Double)e.getValue());
			break;
		case PHERO_RADIUS :
			gridModel.setPheroRadius((Integer)e.getValue());
			break;
		default: throw new Error("Property not managed.");
		}
	}

	@Override
	public void mouseWheelMoved(MouseWheelEvent arg0) {
		float a = (float) Math.pow(1.5, -arg0.getWheelRotation());
		xzoom *= a;
		yzoom *= a;
		completeRedrawAndResize();
	}

	public void moveAgent(Agent<AgentViewProperties, CellViewProperties> agent, Point p){
		Point p2 = agent.getAdditionnalProperty().getLabel().getLocation();
		int w = gridModel.width;
		int h = gridModel.height;
		short x = (short) ((Math.min(Math.max(0, (p2.x+p.x)/xzoom), w-1) - xoffset + w)%w);
		short y = (short) ((Math.min(Math.max(0, (p2.y+p.y)/yzoom), h-1) - yoffset + h)%h);
		short[] pos = new short[]{x, y};
		agent.setPosition(pos);
		if(!props.get(Properties.REAL_TIME, Boolean.class)){
			locateAgent(pos, agent);
		}
	}

	private Integer attractorRadius;
	private Integer attractorForce;
	private PrincipalFrame frame;

	public void addAttractor(int radius, int force, PrincipalFrame principalFrame){
		attractorRadius = radius;
		attractorForce = force;
		frame = principalFrame;
		addMouseListener(this);
	}

	@Override
	public void mouseClicked(MouseEvent arg0) {
		Point p = arg0.getPoint();
		int w = gridModel.width;
		int h = gridModel.height;
		short x = (short) ((Math.max(0, p.x/xzoom) - xoffset + w)%w);
		short y = (short) ((Math.max(0, p.y/yzoom) - yoffset + h)%h);
		short[] pos = new short[]{x, y};
		props.set(Properties.USE_ATTRACTOR, Boolean.class, true);
		props.set(Properties.DRAW_ATTRACTOR, Boolean.class, true);
		gridModel.addAttractor(new Attractor<CellViewProperties>(gridModel.getCellAt(pos), attractorForce, 1f*attractorRadius));
		removeMouseListener(this);
		attractorRadius = null;
		attractorForce = null;
		if(!props.get(Properties.REAL_TIME, Boolean.class)){
			completeRedraw();
		}
		frame.setCursor(Cursor.getDefaultCursor());
	}

	@Override
	public void mouseEntered(MouseEvent arg0) {
	}

	@Override
	public void mouseExited(MouseEvent arg0) {
	}

	@Override
	public void mousePressed(MouseEvent arg0) {
	}

	@Override
	public void mouseReleased(MouseEvent arg0) {
	}

	public void clearAttractors(boolean attractor, boolean phero) {
		gridModel.clearAttractors(attractor, phero);
		if(!props.get(Properties.REAL_TIME, Boolean.class)){
			completeRedraw();
		}
	}
}